/**
 * Copyright © 2011 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.common;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.logging.Level;
import org.creativor.rayson.api.Portable;
import org.creativor.rayson.api.RpcService;
import org.creativor.rayson.exception.CallInvokeException;
import org.creativor.rayson.exception.UnportableTypeException;
import org.creativor.rayson.util.Log;

/**
 * An portable object that carry the RPC calling parameters from client to
 * server.
 * 
 * @author Nick Zhang
 */
public class Invocation implements Portable
{
	/**
	 * Client RPC protocol that invoking this Invocation.
	 */
	private int version;
	private Object[] parameters;
	private int hashCode;
	
	private static final Object[] EMPTY_PARAMETERS = new Object[0];
	/**
	 * An cache of <Method --> RPC call hash code> .
	 */
	private static final HashMap<Method, Integer> HASH_CODE_CACHE = new HashMap<Method, Integer>();
	
	/**
	 * Instantiates a new invocation.
	 */
	public Invocation()
	{
		
	}
	
	/**
	 * Construct a new RPC invocation object. The RPC request hash code will be
	 * initialized. Also, there is java method to RPC request hash code cache
	 * which improved the performance of hash code generation.
	 * 
	 * @param version
	 *            The version of the client protocol which invoking this
	 *            invocation.
	 * @param method
	 *            RPC proxy method which telling what method to call in remote
	 *            RPC service.
	 * @param parameters
	 *            The parameters associated with the RPC method.
	 * @throws UnportableTypeException
	 *             If one of parameter type of method is un-portable.
	 */
	public Invocation(int version, Method method, Object[] parameters)
			throws UnportableTypeException
	{
		this.version=version;
		if (parameters == null)
			this.parameters = EMPTY_PARAMETERS;
		else
		{
			for (Object parameter : parameters)
			{
				if (parameter != null && !Stream.isPortable(parameter.getClass()))
					throw new UnportableTypeException("Paramter type is un-portable");
			}
			this.parameters = parameters;
		}
		// get hash code from cache.
		this.hashCode = getHashCodeFromCache(method);
	}
	
	/**
	 * @param method
	 * @return
	 */
	private static int getHashCodeFromCache(Method method)
	{
		Integer hashCode = HASH_CODE_CACHE.get(method);
		if (hashCode == null)
		{
			hashCode = Invocation.getHashCode(method);
			HASH_CODE_CACHE.put(method, hashCode);
		}
		return hashCode;
	}
	
	/**
	 * Get hash code of one java Method. This hash code will be used to Identify
	 * an RPC call<br>
	 * For example, method M
	 * "Integer sum(int a, Integer b) throws IllegalArgumentException" will got
	 * hash code:<br>
	 * <b> M -> "Integer sum(int,b)" -> hash code</b>
	 * 
	 * @param method
	 *            The method which get hash code from.
	 */
	public static int getHashCode(final Method method)
	{
		if (method == null) throw new IllegalArgumentException("Method should not be null");
		StringBuffer idString = new StringBuffer();
		idString.append(method.getName());
		idString.append("(");
		Class<?>[] parameterTypes = method.getParameterTypes();
		int parameterCount = parameterTypes.length;
		
		for (int i = 0; i < parameterCount; i++)
		{
			idString.append(parameterTypes[i].getName());
			idString.append(",");
		}
		
		if (parameterCount > 0) idString.deleteCharAt(idString.length() - 1);
		idString.append(")");
		return idString.toString().hashCode();
	}
	
	/**
	 * Gets the hash code.
	 * 
	 * @return the hash code
	 */
	public int getHashCode()
	{
		return hashCode;
	}
	
	/**
	 * @return the client protocol version that invoking this Invocation.
	 */
	public int getVersion()
	{
		return version;
	}
	
	/**
	 * Invoke.
	 * 
	 * @param serviceObject
	 *            the service object
	 * @param method
	 *            the method
	 * @return the object
	 * @throws CallInvokeException
	 *             the call invoke exception
	 */
	public Object invoke(RpcService serviceObject, Method method) throws CallInvokeException
	{
		// Class[] realParaTypes = new Class[this.parameters.length + 1];
		// realParaTypes[0] = SESSION_CLASS;
		Object result = null;
		try
		{
			result = method.invoke(serviceObject, this.parameters);
		} catch (InvocationTargetException e)
		{
			Throwable targetException = e.getTargetException();
			boolean declaredException = false;
			Class[] exceptionTypes = method.getExceptionTypes();
			for (Class exceptionType : exceptionTypes)
			{
				if (exceptionType.isAssignableFrom(targetException.getClass()))
				{
					declaredException = true;
					break;
				}
			}
			if (!declaredException)
				Log.getLogger().log(Level.SEVERE, "Invoke rpc throws an undeclared exception",
						targetException);
			throw new CallInvokeException(targetException);
		} catch (Exception e)
		{
			throw new CallInvokeException(e);
		}
		return result;
	}
	
	@Override
	public void read(DataInput in) throws IOException
	{
		this.version = in.readInt();
		this.hashCode = in.readInt();
		byte paraLength = in.readByte();
		this.parameters = new Object[paraLength];
		
		// read parameter object
		for (int i = 0; i < paraLength; i++)
		{
			this.parameters[i] = Stream.readPortable(in);
		}
	}
	
	/**
	 * To string.
	 * 
	 * @return the string
	 */
	@Override
	public String toString()
	{
		StringBuffer sb = new StringBuffer();
		sb.append("{");
		sb.append("hash code: ");
		sb.append(hashCode);
		sb.append(", parameters: [");
		for (Object parameter : parameters)
		{
			sb.append(parameter);
			sb.append(",");
		}
		sb.deleteCharAt(sb.length() - 1);
		sb.append("]");
		sb.append("}");
		return sb.toString();
	}
	
	@Override
	public void write(DataOutput out) throws IOException
	{
		// write client protocol version.
		out.writeInt(version);
		// write hash code
		out.writeInt(hashCode);
		byte paraLength = (byte) parameters.length;
		out.writeByte(paraLength);
		// write paramter objects.
		for (int i = 0; i < paraLength; i++)
		{
			Stream.writePortable(out, this.parameters[i]);
		}
	}
}
